Conversation
07dce85 to
b335a63
Compare
Dereferencing a scoped field rebuilds FieldReference without its outer-reference or lambda metadata. SQL such as i.id = o.s.v inside a correlated subquery consequently exports o.s.v as a local root reference; with matching schemas it instead reads i.s.v without a type error. Copy the original reference metadata while extending its path and deriving the selected type. Struct, list, and map dereferences retain their outer steps, relation anchor, or lambda scope, including a zero-step reference to the current lambda. This fixes reference construction and SQL export. It does not add support for nested scoped paths in reverse converters that currently cannot handle them.
b335a63 to
527eaf7
Compare
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Caution Review failedAn error occurred during the review process. Please try again later. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthrough
ChangesField reference dereferencing
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Suggested reviewers: Merge Risk: 🟡 Moderate · up to Forward conversion preserves reference scope, but reverse conversion can silently change nested correlated references or reject nested lambda references. Handle or explicitly reject unsupported paths before merging to prevent incorrect round-trip results. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change repairs reference binding without demonstrating new access privileges or an authorization bypass. Some reverse conversions can still reinterpret unsupported nested references without rejecting them. Production exposure and end-to-end behavior remain unconfirmed. Retained concerns
Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
nielspardon
left a comment
There was a problem hiding this comment.
Thanks, keeping the scope is right: the spec allows a nested direct_reference under outer_reference and lambda_parameter_reference. The reverse converters don't reject these paths, though. They now read the wrong field without any error, so please add guards (or real handling) in this PR.
|
|
||
| private FieldReference dereference(Type newType, ReferenceSegment nextSegment) { | ||
| return ImmutableFieldReference.builder() | ||
| .from(this) |
There was a problem hiding this comment.
Reject nested scoped paths in these readers, or handle them. Before this change they threw on these references; now they return a wrong result. These lines are outside the diff, so I couldn't attach suggestions:
ExpressionRexConverter.java:834(outer) and:863(lambda) takesegments().get(0), which is the innermost step. The new isthmus query converts back to Calcite asi.id = $cor0.ID, and a lambdax.f1becomes(p0, p1) -> p1. Throwing whensegments().size() > 1covers both.ProtoExpressionConverter.java:94reads only the topstruct_fieldand drops itschild, so the exported plan reads back asi.id = o.s(INTEGER vs ROW). The lambda case at:119already throws onhasChild(); do the same here.
An assertProtoPlanRoundrip on the new isthmus query would have caught the second one.
There was a problem hiding this comment.
Thanks for tracing the consumer paths. The local patch adds guards in both readers and regressions for nested outer and lambda references, including matching-type fields that previously selected the wrong field without an error. Unsupported paths now fail explicitly. The full build passes, and the update is not pushed yet.
| case OUTER_ANCHOR: | ||
| builder.outerReferenceRelReference(7); | ||
| break; | ||
| case LAMBDA_CURRENT: |
There was a problem hiding this comment.
A dereferenced lambda parameter now writes a nested lambda_parameter_reference path, which ProtoExpressionConverter:119 still rejects, so these plans no longer survive POJO → proto → POJO. That reader side is item 3 of #1322; either handle it here or note it as a known gap in the PR description.
There was a problem hiding this comment.
Agreed. I prepared description text that explicitly keeps nested lambda parameter reading unsupported and points to #1322. This change preserves those paths during construction and export, while rejecting them where readers cannot handle them. The description update is pending publication.
| FieldReference reference = reference(scope, R.struct(R.BOOLEAN, N.I64)); | ||
|
|
||
| assertDereference( | ||
| reference, reference.dereferenceStruct(1), N.I64, FieldReference.StructField.of(1)); |
There was a problem hiding this comment.
Dereference a different field than the base's StructField(1), so the expected list isn't symmetric and a segment-order regression fails this case too.
| reference, reference.dereferenceStruct(1), N.I64, FieldReference.StructField.of(1)); | |
| reference, reference.dereferenceStruct(0), R.BOOLEAN, FieldReference.StructField.of(0)); |
There was a problem hiding this comment.
Thanks, I changed the local struct case to child index 0 and R.BOOLEAN, so both the offset and type distinguish the new segment from the base reference. This is included in the pending update.
| assertEquals(expectedType, dereferenced.getType()); | ||
| assertEquals(List.of(nextSegment, original.segments().get(0)), dereferenced.segments()); | ||
| assertEquals(original.inputExpression(), dereferenced.inputExpression()); | ||
| assertEquals(original.outerReferenceStepsOut(), dereferenced.outerReferenceStepsOut()); | ||
| assertEquals(original.outerReferenceRelReference(), dereferenced.outerReferenceRelReference()); | ||
| assertEquals( | ||
| original.lambdaParameterReferenceStepsOut(), | ||
| dereferenced.lambdaParameterReferenceStepsOut()); | ||
|
|
||
| io.substrait.proto.Expression.FieldReference originalProto = | ||
| expressionProtoConverter.toProto(original).getSelection(); | ||
| io.substrait.proto.Expression.FieldReference dereferencedProto = | ||
| expressionProtoConverter.toProto(dereferenced).getSelection(); | ||
| assertEquals(originalProto.getRootTypeCase(), dereferencedProto.getRootTypeCase()); | ||
| assertEquals(originalProto.getOuterReference(), dereferencedProto.getOuterReference()); | ||
| assertEquals( | ||
| originalProto.getLambdaParameterReference(), | ||
| dereferencedProto.getLambdaParameterReference()); |
There was a problem hiding this comment.
Compare the whole object instead of listing attributes by hand, so a newly added attribute that dereference drops is caught. Also remove the then-unused java.util.List import.
| assertEquals(expectedType, dereferenced.getType()); | |
| assertEquals(List.of(nextSegment, original.segments().get(0)), dereferenced.segments()); | |
| assertEquals(original.inputExpression(), dereferenced.inputExpression()); | |
| assertEquals(original.outerReferenceStepsOut(), dereferenced.outerReferenceStepsOut()); | |
| assertEquals(original.outerReferenceRelReference(), dereferenced.outerReferenceRelReference()); | |
| assertEquals( | |
| original.lambdaParameterReferenceStepsOut(), | |
| dereferenced.lambdaParameterReferenceStepsOut()); | |
| io.substrait.proto.Expression.FieldReference originalProto = | |
| expressionProtoConverter.toProto(original).getSelection(); | |
| io.substrait.proto.Expression.FieldReference dereferencedProto = | |
| expressionProtoConverter.toProto(dereferenced).getSelection(); | |
| assertEquals(originalProto.getRootTypeCase(), dereferencedProto.getRootTypeCase()); | |
| assertEquals(originalProto.getOuterReference(), dereferencedProto.getOuterReference()); | |
| assertEquals( | |
| originalProto.getLambdaParameterReference(), | |
| dereferencedProto.getLambdaParameterReference()); | |
| assertEquals( | |
| ImmutableFieldReference.copyOf(original) | |
| .withType(expectedType) | |
| .withSegments(nextSegment, original.segments().get(0)), | |
| dereferenced); |
There was a problem hiding this comment.
Thanks, I switched the local test to whole-reference equality with copyOf, withType, and withSegments, and removed the unused List import. The branch update is pending.
| } | ||
|
|
||
| private FieldReference reference(ReferenceScope scope, Type type) { | ||
| ImmutableFieldReference.Builder builder = |
There was a problem hiding this comment.
Nit: build these with the existing factories (newRootStructReference, newStructReference, newRootStructOuterReference, newRootStructOuterReferenceByRelReference, newLambdaParameterReference) in a switch expression, which also drops the unreachable default.
There was a problem hiding this comment.
Agreed, I updated the local fixture to use the reference factories through a switch expression, including the lambda factory in this package. This is included in the pending update.
| import org.apache.calcite.sql.parser.SqlParseException; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class CorrelatedNestedFieldTest { |
There was a problem hiding this comment.
Nit: move this into SubqueryPlanTest as a @Test using toProto(toSubstraitPlan(sql, catalog)), since it repeats that class's correlated-EXISTS navigation step for step.
There was a problem hiding this comment.
Thanks, I moved the local regression into SubqueryPlanTest and reused the shared conversion helpers. The reverse-reader assertions now sit alongside the export regression. The full build passes; these changes are not pushed yet.
|
@coderabbitai resume |
✅ Action performedReviews resumed and review finished. |
Reject nested outer and lambda paths before reverse conversion can select the wrong field. Cover the reader failures and simplify scoped dereference regressions.
Dereferencing a scoped field rebuilds FieldReference without its outer-reference or lambda metadata. SQL such as i.id = o.s.v inside a correlated subquery consequently exports o.s.v as a local root reference; with matching schemas it instead reads i.s.v without a type error.
Copy the original reference metadata while extending its path and deriving the selected type. Struct, list, and map dereferences retain their outer steps, relation anchor, or lambda scope, including a zero-step reference to the current lambda.
Nested outer and lambda field paths remain unsupported when converting protobuf back to POJOs or POJOs back to Calcite. Reject these paths explicitly instead of dropping segments or selecting another field. Reading nested lambda parameter paths remains tracked in #1322.
Summary by CodeRabbit